//////////////////////////////////////////////////////////////////////////////////////////
// Paging Manager class for EGTB Reader
//////////////////////////////////////////////////////////////////////////////////////////

#include "stdafx.h"

#include "PagingManager.h"
#include "wrap.h"

namespace DbReader
{
	// Constructor
	PagingManager::PagingManager(int32 iCacheSizeMB)
	{
		m_iCacheSizeMB = iCacheSizeMB;
		m_nPageSize = 0;
		m_nBlockCount = 0;
		m_nCacheBlockCount = 0;
		m_nFileCount = 0;
		m_nHead = 0;
		m_nTail = 0;
		m_nCompressionType = 0;

		m_nCurrentBlockNumber = 0;
		m_nIndexInBlock = 0;
		m_pCompressedFile = NULL;
		m_nTotalBlockCount = 0;
		m_nIndexFileOffset = 0;		 
		m_pFileBlockOffsets = NULL;
		m_pCompressBuffer = NULL;

		m_pDataCache = NULL;
		m_pBlockPointers = NULL;
		m_pFiles = NULL;
		m_pLRU = NULL;
		m_pTmpBlock = NULL;
	}

	// Destructor - clean up
	PagingManager::~PagingManager()
	{
		if(m_pDataCache != NULL)
		{
			delete[] m_pDataCache;
			m_pDataCache = NULL;
		}
		if(m_pBlockPointers != NULL)
		{
			delete[] m_pBlockPointers;
			m_pBlockPointers = NULL;
		}
		if(m_pFiles != NULL)
		{
			delete[] m_pFiles;
			m_pFiles = NULL;
		}
		if(m_pLRU != NULL)
		{
			delete m_pLRU;
			m_pLRU = NULL;
		}
		if(m_pTmpBlock != NULL)
		{
			delete m_pTmpBlock;
			m_pTmpBlock = NULL;
		}
		if(m_pFileBlockOffsets != NULL)
		{
			delete[] m_pFileBlockOffsets;
			m_pFileBlockOffsets = NULL;
		}
		if(m_pCompressBuffer != NULL)
		{
			delete[] m_pCompressBuffer;
			m_pCompressBuffer = NULL;
		}
	}


	// The public API methods
	/////////////////////////////////////////////////

	// Initialization of paging subsystem
	bool PagingManager::Initialize(int fileCount, EGTBFile* pFiles, int32 nCompressionType)
	{
		m_nCompressionType = nCompressionType;

		// initialize file array
		m_pFiles = new FilePagingRecord[fileCount];
		memset(m_pFiles, 0, sizeof(FilePagingRecord) * fileCount);

		// load file information
		int32 blockCount = 0;
		for(int n = 0; n < fileCount; n++)
		{
			if(LoadFileHeaderInfo(pFiles + n, n) == false)
				return false;
			
			// calculate block counts
			m_pFiles[n].nStartingBlock = blockCount;
			pFiles[n].nGlobalBlockNumber = blockCount;
			blockCount += m_pFiles[n].nBlockCount;
		}

		// init data
		m_nFileCount = fileCount;

		// intialize cache
		InitCache();

		// init LRU
		InitLRU();

		// succcess
		return true;
	}

	// returns the requested block
	uint8* PagingManager::GetBlock(int fileIndex, int nGlobalBlockNumber)
	{	
		int32 nLRUIndex = 0;
		uint8* pData = m_pBlockPointers[nGlobalBlockNumber];
		assert(nGlobalBlockNumber < m_nBlockCount);
		if(pData != NULL)
		{
			// calculate LRU index
			nLRUIndex = (pData - m_pDataCache) / m_nPageSize;			
			
			// move in the LRU list to the top
			MoveLRUItemToHead(nLRUIndex);
			
			return pData;
		}
		else
		{
			// find LRU index
			nLRUIndex = FindLRUBlock();

			// read disk block
			uint32 nLocalBlockIndex = nGlobalBlockNumber - m_pFiles[fileIndex].nStartingBlock;	
			int32 compressedSize = ReadBlock(fileIndex, nLocalBlockIndex, m_pTmpBlock);

			// decompress block
			uint8* pBlock = m_pDataCache + nLRUIndex * m_nPageSize;
			UncompressBlock(m_pTmpBlock, pBlock, compressedSize);

			// set the block pointer
			m_pBlockPointers[nGlobalBlockNumber] = pBlock;
			m_pLRU[nLRUIndex].nBlockNumber = nGlobalBlockNumber;

			// move to the top
			MoveLRUItemToHead(nLRUIndex);

			return pBlock;
		}
	}

	// initializes PagingManager into the compression mode
	void PagingManager::InitializeCompressionMode(FILE* fp, int32 nCompressionType, uint32 nTotalBlockCount, int32 nPageSize)
	{
		// set variables
		m_pCompressedFile = fp;
		m_nCompressionType = nCompressionType;
		m_nTotalBlockCount = nTotalBlockCount;
		m_nPageSize = nPageSize;

		// init empty buffer for compression
		m_pTmpBlock = new uint8[m_nPageSize];
		m_pCompressBuffer = new uint8[m_nPageSize];
		
		// calculate space for indexes
		uint64 indexsize = sizeof(uint64) * (m_nTotalBlockCount + 1);
		m_pFileBlockOffsets = new uint64[m_nTotalBlockCount + 1];
		memset(m_pFileBlockOffsets, 0, indexsize);

		// fill in paging header
		FilePagingHeader header;
		memset(&header, 0, sizeof(FilePagingHeader));
		header.iPageSize = m_nPageSize;
		header.iBlockCount = m_nTotalBlockCount;
		header.iIndexOffset = sizeof(FilePagingHeader);
		header.iDataOffset = header.iIndexOffset + indexsize;

		// write the paging header
		fwrite(&header, sizeof(FilePagingHeader), (size_t)1, m_pCompressedFile);

		// save offset for writing indexes
		m_nIndexFileOffset = _ftelli64(m_pCompressedFile);

		// write indexes
		fwrite(m_pFileBlockOffsets, indexsize, (size_t)1, m_pCompressedFile);
	}

	// writes one byte to the stream of compressed DB, either WDL or DTM
	// invokes compression when the buffer is empty
	void PagingManager::WriteByte(uint8 value)
	{
		// save the value
		m_pTmpBlock[m_nIndexInBlock++] = value;

		// check the block size
		if(m_nIndexInBlock >= m_nPageSize) // if buffer is full
		{
			// write the last page
			CompressAndWritePage();
		} // if
	}

	// finalizes the compression, that is, saves the last block, updates indexes 
	// and saves them
	void PagingManager::FinalizeCompression()
	{
		if(m_nIndexInBlock != 0) // if there are remaining data in the temporary buffer
		{
			// write the last page
			CompressAndWritePage();
		}

		// finally save indexes

		// seek to the index file offset
		_fseeki64(m_pCompressedFile, m_nIndexFileOffset, SEEK_SET);

		// write the indexes
		uint64 indexsize = sizeof(uint64) * (m_nTotalBlockCount + 1);
		fwrite(m_pFileBlockOffsets, indexsize, (size_t)1, m_pCompressedFile);
	}

	// Private methods
	/////////////////////////////////////////////////

	// compresses and writes the page
	void PagingManager::CompressAndWritePage()
	{
		// compress the block
		int nCompressedSize = CompressBlock(m_pTmpBlock, m_pCompressBuffer);

		// save the block
		fwrite(m_pCompressBuffer, (size_t)nCompressedSize, (size_t)1, m_pCompressedFile);

		// update the index
		uint64 offset = (uint64)nCompressedSize + m_pFileBlockOffsets[m_nCurrentBlockNumber];
		m_pFileBlockOffsets[++m_nCurrentBlockNumber] = offset;

		// reset the index in the block
		m_nIndexInBlock = 0;
			
		// check index overflow
		assert(m_nCurrentBlockNumber <= m_nTotalBlockCount);
	}



	// loads the EGTB file header and initializes the data structures
	bool PagingManager::LoadFileHeaderInfo(EGTBFile* file, int nFileIndex)
	{
		// seek to the paging data
		int result = _fseeki64(file->fp, file->nPageDataOffset, SEEK_SET);
		if(result)
			return false;

		// load Paging header
		FilePagingHeader header;
		memset(&header, 0, sizeof(FilePagingHeader));
		if(!fread_s(&header, sizeof(FilePagingHeader), (size_t)1, sizeof(FilePagingHeader), file->fp))
			return false;

		// initialize internal variables
		m_nPageSize = header.iPageSize;
		m_pFiles[nFileIndex].fp = file->fp;
		m_pFiles[nFileIndex].nBlockCount = header.iBlockCount;
		m_pFiles[nFileIndex].nCompressedDataOffset = header.iDataOffset + file->nPageDataOffset;
		
		// there is one index more to check the size of the last block in the EGTB file
		m_pFiles[nFileIndex].pBlockOffsets = new uint64[header.iBlockCount + 1]; 

		// seek to the indexes
		result = _fseeki64(file->fp, file->nPageDataOffset + header.iIndexOffset, SEEK_SET);
		if(result)
			return false;

		// read the block file offsets
		if(!fread(m_pFiles[nFileIndex].pBlockOffsets,(size_t)1, 
			sizeof(uint64) * (header.iBlockCount+1), file->fp))
		{
			return false;
		}

		// success 
		return true;
	}

	// initializes data cache and sets initial values of some variables
	void PagingManager::InitCache()
	{
		// init some variables
		m_nCacheBlockCount = ((uint64)m_iCacheSizeMB * 1024 * 1024) / (uint64)m_nPageSize;

		// calculate the total block size of the all EGTB database
		m_nBlockCount = 0;
		for(int i = 0; i < m_nFileCount; i++)
		{
			m_nBlockCount += m_pFiles[i].nBlockCount;
		} // for

		// allocate space for cache
		uint64 memsize = m_nCacheBlockCount * m_nPageSize;
		m_pDataCache = new uint8[memsize];
		memset(m_pDataCache, 0, memsize);

		// allocate the block pointer array - for mapping block number to the cache
		m_pBlockPointers = new uint8*[m_nBlockCount];
		memset(m_pBlockPointers, 0, sizeof(uint8*) * m_nBlockCount);

		// allocate temporary block for compression / decompression routines
		m_pTmpBlock = new uint8[m_nPageSize];
	}

	// initializes LRU double-linked list of blocks
	void PagingManager::InitLRU()
	{
		// allocate space
		m_pLRU = new LRUListItem[m_nCacheBlockCount];

		// set global pointers
		m_nHead = EMPTY_LRU;
		m_nTail = EMPTY_LRU;
		m_nLast = EMPTY_LRU;

		// unintialize list
		for(int i = 0; i < m_nCacheBlockCount; i++)
		{
			m_pLRU[i].nBlockNumber = EMPTY_LRU;
			m_pLRU[i].nPrevItem = EMPTY_LRU;
			m_pLRU[i].nNextItem = EMPTY_LRU;				 
		}
	}

	// moves the LRU item to the top 
	// nIndex:  is the index of block in the cache
	void PagingManager::MoveLRUItemToHead(int nIndex)
	{
		if(m_nHead == -1)
		{
			m_nHead = nIndex;
			m_nTail = nIndex;
			m_nLast = nIndex;
			return;
		}

		// if already on top, exit
		if(nIndex == m_nHead)
			return;
		
		if(nIndex == m_nTail)
		{
			// if remove from the end of list

			// set previos item to the tail
			m_nTail = m_pLRU[nIndex].nPrevItem;
			
			// set the tail LRU inext item to the empty
			m_pLRU[m_nTail].nNextItem = EMPTY_LRU;		
		}
		else
		{
			// remove from the inside of list

			// get previous and next items of moved item
			int32 prev = m_pLRU[nIndex].nPrevItem;
			int32 next = m_pLRU[nIndex].nNextItem;

			// remove form the list
			m_pLRU[prev].nNextItem = next;
			if(next != EMPTY_LRU)
				m_pLRU[next].nPrevItem = prev;
			else
				m_nTail = prev;
		}

		// set previos to the empty
		m_pLRU[nIndex].nPrevItem = EMPTY_LRU;

		m_pLRU[m_nHead].nPrevItem = nIndex;

		// set the next item to the current head
		m_pLRU[nIndex].nNextItem = m_nHead;
		// set finally the head
		m_nHead = nIndex;
	}

	// find the least recently used block
	int32 PagingManager::FindLRUBlock()
	{
		// if empty tail, return the first block
		if(m_nLast == EMPTY_LRU)
			return 0;

		// if cache is full, return the last block
		if(m_nLast == m_nCacheBlockCount - 1)
		{
			// reset index to the cache
			m_pBlockPointers[m_pLRU[m_nTail].nBlockNumber] = NULL;
			
			return m_nTail;
		}
		else
		{
			// cache is not full, read the next empty item
			m_pLRU[m_nTail].nNextItem = m_nLast+1;
			m_pLRU[m_nLast+1].nPrevItem = m_nTail;
			m_nLast++;
			// m_nTail = m_nLast;
			return m_nLast;			
		}
	}

	// compresses the block - returns the size
	int32 PagingManager::CompressBlock(uint8* pSrc, uint8* pDst)
	{
		size_t compressedSize = 0;

		// choose selected compression type
		switch(m_nCompressionType)		
		{
		case CPR_JUST_COPY:
			justcopy_encode(pSrc, m_nPageSize, pDst, &compressedSize, m_nPageSize);
			break;
		case CPR_RLE:
			rle_encode(pSrc, m_nPageSize, pDst, &compressedSize, m_nPageSize);
			break;
		case CPR_LZF:
			lzf_encode(pSrc, m_nPageSize, pDst, &compressedSize, m_nPageSize);
			break;
		case CPR_LZMA:
			lzma_encode(pSrc, m_nPageSize, pDst, &compressedSize, m_nPageSize);
			break;
		case CPR_ZIP:
			zlib_encode(pSrc, m_nPageSize, pDst, &compressedSize, m_nPageSize);
			break;
		}

		return compressedSize;
	}

	// decompresses block
	void PagingManager::UncompressBlock(uint8* pSrc, uint8* pDst, size_t compressedSize)
	{
		size_t dummy;

		// choose selected compression type
		switch(m_nCompressionType)		
		{
		case CPR_JUST_COPY:
			justcopy_decode(pSrc, compressedSize, pDst, &dummy, m_nPageSize);
			break;
		case CPR_RLE:
			rle_decode(pSrc, compressedSize, pDst, &dummy, m_nPageSize);
			break;
		case CPR_LZF:
			lzf_decode(pSrc, compressedSize, pDst, &dummy, m_nPageSize);
			break;
		case CPR_LZMA:
			lzma_decode(pSrc, compressedSize, pDst, &dummy, m_nPageSize);
			break;
		case CPR_ZIP:
			zlib_decode(pSrc, compressedSize, pDst, &dummy, m_nPageSize);
			break;
		}
	}

	// reads block with the given local file block number
	// returns the block size
	int32 PagingManager::ReadBlock(int32 nFileIndex, int32 nLocalBlockNumber, uint8* pOutput)
	{
		// get the block local offset
		uint64 blockOffset = m_pFiles[nFileIndex].pBlockOffsets[nLocalBlockNumber];

		// calculate the block size
		uint32 blockSize = (uint32)(m_pFiles[nFileIndex].pBlockOffsets[nLocalBlockNumber + 1] - blockOffset);

		// make the file offset global
		blockOffset += m_pFiles[nFileIndex].nCompressedDataOffset;

		// seek in the file
		_fseeki64(m_pFiles[nFileIndex].fp, blockOffset, SEEK_SET);

		// read the block
		fread(pOutput, (size_t)1, blockSize, m_pFiles[nFileIndex].fp);

		// return the size
		return blockSize;
	}
}